﻿using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;

namespace ICodeShare.UI.Controls
{
    /// <summary>
    /// 跳动数字
    /// </summary>
    [TemplatePart(Name = PART_Root, Type = typeof(Grid))]
    [TemplatePart(Name = PART_Container, Type = typeof(StackPanel))]
    public class Odometer : ContentControl
    {
        #region Constant

        private const string PART_Root = "PART_Root"; //主要部分
        private const string PART_Container = "PART_Container"; //主要部分

        #endregion Constant

        #region Members

        /// <summary>
        /// All digits for the number in the order most significant to least
        /// </summary>
        private List<OdometerDigit> digitCollection = new List<OdometerDigit>();

        /// <summary>
        /// storyboard to act as a timer
        /// </summary>
        private DispatcherTimer timer = new DispatcherTimer();

        private Grid gridRoot;
        private StackPanel stpContainer;

        #endregion Members

        #region Dependency Properties

        #region Digits 数字个数

        /// <summary>
        /// Dependancy property for our Digits property
        /// </summary>
        public static readonly DependencyProperty DigitsProperty =
            DependencyProperty.Register("Digits", typeof(double), typeof(Odometer), new PropertyMetadata(1.0, new PropertyChangedCallback(DigitsPropertyChanged)));

        /// <summary>
        /// Gets or sets number of digits to display
        /// </summary>
        public double Digits
        {
            get { return (double)GetValue(DigitsProperty); }
            set { SetValue(DigitsProperty, value); }
        }

        /// <summary>
        /// Our Digits dependany property has changed, deal with it
        /// </summary>
        /// <param name="dependancy">the dependancy object</param>
        /// <param name="args">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
        private static void DigitsPropertyChanged(DependencyObject dependancy, DependencyPropertyChangedEventArgs args)
        {
            Odometer instance = dependancy as Odometer;
            if (instance != null && instance.DashboardLoaded)
            {
                instance.UpdateDigits();
            }
        }

        #endregion Digits 数字个数

        #region FinalValue 最终值

        /// <summary>
        /// Dependancy property for the FinalValue
        /// </summary>
        public static readonly DependencyProperty FinalValueProperty =
            DependencyProperty.Register("FinalValue", typeof(int), typeof(Odometer), new PropertyMetadata(9));

        /// <summary>
        /// Gets or sets the final value for the Odometer when executing in the MeterMode.InitialToFinal.
        /// </summary>
        /// <value>The final value.</value>
        public int FinalValue
        {
            get { return (int)GetValue(FinalValueProperty); }
            set { SetValue(FinalValueProperty, value); }
        }

        #endregion FinalValue 最终值

        #region InitialValue 初始值

        /// <summary>
        /// Dependancy property for the InitialValue of the Odeometer
        /// </summary>
        public static readonly DependencyProperty InitialValueProperty =
            DependencyProperty.Register("InitialValue", typeof(int), typeof(Odometer), new PropertyMetadata(0, new PropertyChangedCallback(InitialValuePropertyChanged)));

        /// <summary>
        /// Gets or sets the initial value for the Odometer when executing in the MeterMode.InitialToFinal.
        /// </summary>
        public int InitialValue
        {
            get { return (int)GetValue(InitialValueProperty); }
            set { SetValue(InitialValueProperty, value); }
        }

        /// <summary>
        /// Our InitialValue property has changed, deal with it
        /// </summary>
        /// <param name="dependancy">the dependancy object</param>
        /// <param name="args">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
        private static void InitialValuePropertyChanged(DependencyObject dependancy, DependencyPropertyChangedEventArgs args)
        {
            Odometer instance = dependancy as Odometer;
            if (instance != null && instance.DashboardLoaded)
            {
                instance.UpdateInitialValue();
            }
        }

        #endregion InitialValue 初始值

        #region Interval 时间间隔

        /// <summary>
        /// The dependancy property for the Interval property
        /// </summary>
        public static readonly DependencyProperty IntervalProperty =
            DependencyProperty.Register("Interval", typeof(double), typeof(Odometer), new PropertyMetadata(1.0, new PropertyChangedCallback(IntervalPropertyChanged)));

        /// <summary>
        /// Gets or sets the time in seconds between ticks for automatic dials (can be fractional)
        /// </summary>
        public double Interval
        {
            get { return (double)GetValue(IntervalProperty); }
            set { SetValue(IntervalProperty, value); }
        }

        /// <summary>
        /// The interval dependany property changed.
        /// </summary>
        /// <param name="dependancy">The dependancy.</param>
        /// <param name="args">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
        private static void IntervalPropertyChanged(DependencyObject dependancy, DependencyPropertyChangedEventArgs args)
        {
            Odometer instance = dependancy as Odometer;
            if (instance != null && instance.DashboardLoaded)
            {
                instance.UpdateInterval();
            }
        }

        #endregion Interval 时间间隔

        #region Animate 数字是否变动

        /// <summary>
        /// The dependancy property for the Interval property
        /// </summary>
        public static readonly DependencyProperty AnimateProperty =
            DependencyProperty.Register("Animate", typeof(bool), typeof(Odometer), new PropertyMetadata(true, new PropertyChangedCallback(AnimatePropertyChanged)));

        /// <summary>
        /// Gets or sets the time in seconds between ticks for automatic dials (can be fractional)
        /// </summary>
        public bool Animate
        {
            get { return (bool)GetValue(AnimateProperty); }
            set { SetValue(AnimateProperty, value); }
        }

        /// <summary>
        /// The interval dependany property changed.
        /// </summary>
        /// <param name="dependancy">The dependancy.</param>
        /// <param name="args">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
        private static void AnimatePropertyChanged(DependencyObject dependancy, DependencyPropertyChangedEventArgs args)
        {
            Odometer instance = dependancy as Odometer;
            if (instance != null && instance.DashboardLoaded)
            {
                instance.UpdateTimerEnable();
            }
        }

        #endregion Animate 数字是否变动

        #region MeterMode 计数模式

        /// <summary>
        /// The dependancy property for the MeterModelColor property
        /// </summary>
        public static readonly DependencyProperty MeterModeProperty =
            DependencyProperty.Register("MeterMode", typeof(IncrementDecrementMode), typeof(Odometer), new PropertyMetadata(IncrementDecrementMode.AutoIncrement));

        /// <summary>
        /// Gets or sets the meter mode.
        /// </summary>
        /// <value>The meter mode.</value>
        public IncrementDecrementMode MeterMode
        {
            get { return (IncrementDecrementMode)GetValue(MeterModeProperty); }
            set { SetValue(MeterModeProperty, value); }
        }

        #endregion MeterMode 计数模式

        #endregion Dependency Properties

        #region Constructors

        static Odometer()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(Odometer), new FrameworkPropertyMetadata(typeof(Odometer)));
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            gridRoot = GetTemplateChild(PART_Root) as Grid;
            stpContainer = GetTemplateChild(PART_Container) as StackPanel;
        }

        protected override void OnInitialized(EventArgs e)
        {
            base.OnInitialized(e);
            Loaded += new RoutedEventHandler(this.Odometer_Loaded);
            this.timer.Interval = new TimeSpan(0, 0, 0, 0, 750);
            this.timer.Tick += new EventHandler(this.Timer_Tick);
        }

        #endregion Constructors

        #region Properties

        /// <summary>
        /// Gets or sets a value indicating whether the dashboard is loaded.
        /// </summary>
        /// <value><c>true</c> if [dashboard loaded]; otherwise, <c>false</c>.</value>
        public bool DashboardLoaded
        {
            get;
            set;
        }

        /// <summary>
        /// Gets the current value.
        /// </summary>
        /// <value>The current value.</value>
        private int CurrentValue
        {
            get
            {
                int res = 0;

                for (int i = 0; i < this.digitCollection.Count; i++)
                {
                    if (i > 0)
                    {
                        res = res * 10;
                    }

                    res += this.digitCollection[i].Digit;
                }

                return res;
            }
        }

        #endregion Properties

        #region Methods

        /// <summary>
        /// Subtracts one from the value of the whole odometer
        /// </summary>
        public void Decrement()
        {
            this.Decrement(CountingUnit.Units);
        }

        /// <summary>
        /// When writing games and using the Odometer as a score control, you
        /// may wish to decrement the score by a thousand rather than 1.This
        /// method allows you to pass in an IncrementDigit specifying which
        /// digit to rollover.
        /// </summary>
        /// <param name="digit">the digit to move</param>
        public void Decrement(CountingUnit digit)
        {
            int digitIndex = (int)digit;
            if (this.digitCollection.Count > digitIndex - 1)
            {
                this.digitCollection[this.digitCollection.Count - digitIndex].Decrement();
            }
        }

        /// <summary>
        /// Adds one to the value of the whole odometer, this is the equivilent to
        /// calling Increment(IncrementDigit.Ones);
        /// </summary>
        public void Increment()
        {
            this.Increment(CountingUnit.Units);
        }

        /// <summary>
        /// When writing games and using the Odometer as a score control, you
        /// may wish to increment the score by a thousand rather than 1.This
        /// method allows you to pass in an IncrementDigit specifying which
        /// digit to rollover.
        /// </summary>
        /// <param name="digit">the digit to move</param>
        public void Increment(CountingUnit digit)
        {
            int digitIndex = (int)digit;
            if (this.digitCollection.Count > digitIndex - 1)
            {
                this.digitCollection[this.digitCollection.Count - digitIndex].Increment();
            }
        }

        /// <summary>
        /// Manifests the changes.
        /// </summary>
        private void ManifestChanges()
        {
            this.UpdateDigits();
            this.UpdateInitialValue();
            this.UpdateInterval();
        }

        /// <summary>
        /// Calculates an changes the value from initial to final in singa increments or decrements
        /// </summary>
        private void MoveFromInitialToFinal()
        {
            if (this.InitialValue < this.FinalValue)
            {
                if (this.CurrentValue < this.FinalValue)
                {
                    this.Increment();
                }
            }
            else if (this.FinalValue < this.InitialValue)
            {
                if (this.CurrentValue > this.FinalValue)
                {
                    this.Decrement();
                }
            }
        }

        /// <summary>
        /// Handles the Loaded event of the Odometer control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> instance containing the event data.</param>
        private void Odometer_Loaded(object sender, RoutedEventArgs e)
        {
            this.ManifestChanges();
            this.DashboardLoaded = true;
            if (this.MeterMode != IncrementDecrementMode.Static)
            {
                this.timer.Start();
            }
        }

        /// <summary>
        /// Handles the Tick event of the timer control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void Timer_Tick(object sender, EventArgs e)
        {
            switch (this.MeterMode)
            {
                case IncrementDecrementMode.Static:
                    break;

                case IncrementDecrementMode.AutoIncrement:
                    this.Increment();
                    break;

                case IncrementDecrementMode.AutoDecrement:
                    this.Decrement();
                    break;

                case IncrementDecrementMode.InitialToFinal:
                    this.MoveFromInitialToFinal();
                    break;
            }
        }

        /// <summary>
        /// Set up the digits, we only do this if the digits are set before the
        /// value. If the value is set first we infer the number of digits from
        /// the value
        /// </summary>
        private void UpdateDigits()
        {
            if (this.digitCollection.Count == 0)
            {
                if (stpContainer != null)
                {
                    stpContainer.Children.Clear();
                    OdometerDigit lastDigit = null;
                    for (int i = 0; i < (int)this.Digits; i++)
                    {
                        OdometerDigit digit = new OdometerDigit();
                        if (lastDigit != null)
                        {
                            digit.DecadePlus += new EventHandler<EventArgs>(lastDigit.LowerOrderDigitDecadePlus);
                            digit.DecadeMinus += new EventHandler<EventArgs>(lastDigit.LowerOrderDigitDecadeMinus);
                        }

                        lastDigit = digit;
                        this.digitCollection.Add(digit);
                        stpContainer.Children.Add(digit);
                    }
                }
            }
        }

        /// <summary>
        /// Puts the digits into their initial states, We expand the total number of
        /// digits if the amount present is not enough
        /// </summary>
        private void UpdateInitialValue()
        {
            double val = this.InitialValue;
            double neededDigits = (Math.Log10(this.InitialValue) + 1) / Math.Log10(10);

            if (this.Digits < neededDigits)
            {
                this.digitCollection.Clear();
                this.Digits = neededDigits;
                this.UpdateDigits();
            }

            for (int i = this.digitCollection.Count; i > 0; i--)
            {
                double d = val % 10;
                OdometerDigit dg = this.digitCollection[i - 1];
                dg.SetInitialValue((int)d);
                val = val / 10;
            }
        }

        /// <summary>
        /// Updates the interval.
        /// </summary>
        private void UpdateInterval()
        {
            this.timer.Interval = TimeSpan.FromSeconds(this.Interval);
        }

        private void UpdateTimerEnable()
        {
            this.timer.IsEnabled = this.Animate;
        }

        #endregion Methods
    }
}